package cn.nawang.ebeim.server.transfer;

import cn.nawang.ebeim.server.constants.Config;
import cn.nawang.ebeim.server.constants.Constant;
import cn.nawang.ebeim.server.service.StorageServiceFactory;
import cn.nawang.ebeim.server.util.PathBuilder;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.SocketAddress;
import java.util.List;
import java.util.Map;

import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpResponseStatus.*;

public class TransferServerHandler extends SimpleChannelInboundHandler<HttpObject> {

    private static final Logger LOG = LoggerFactory.getLogger(TransferServerHandler.class);

    private static final String PRE_UPLOAD_FILE_DIR = Config.WORKING_DIR + Constant.FILE_UPLOAD + File.separator + Constant.TEMP_DS_FOLDER + File.separator;

    /**
     * 临时文件
     */
    private File tempFile;

    /**
     * 最终文件
     */
    private File realFile;

    /**
     * 文件总长度
     */
    private long totalFileLength;

    /**
     * 已接收的文件长度
     */
    private long receiveTotal;

    /**
     * 是否断点续传
     */
    private boolean isBreakPointUpload;

    /**
     * 客户端已经下载的文件的字节数
     */
    private long downLength;

    private RandomAccessFile randomFile;

    private static final int MAX_PING_TIMES = 12;

    private static final int MAX_PONG_TIMES = 6;

    private int pingTimes = 0;

    private String requestUri = "";

    private SocketAddress clientAddress;

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        LOG.error("unCaught error: ",cause);
        writeResponse(ctx, INTERNAL_SERVER_ERROR, "unCaught error");
    }

    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        this.clientAddress = ctx.channel().remoteAddress();
    }

    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        IOUtils.closeQuietly(randomFile);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (IdleState.READER_IDLE == event.state() && requestUri.startsWith("upload")) {
                if (pingTimes > MAX_PONG_TIMES) {
                    if (receiveTotal == totalFileLength) {
                        tryComplete(ctx);
                    } else {
                        LOG.warn("===服务端===(READER_IDLE 读超时): {},{}" , clientAddress,pingTimes);
                        writeResponse(ctx, BAD_REQUEST, "SERVER READER_IDLE");
                    }
                } else {
                    LOG.debug("Got ping response for client:{},{},{}", clientAddress, event.state(),pingTimes);
                    pingTimes++;
                }
            }

            if (IdleState.WRITER_IDLE == event.state() && requestUri.startsWith("download")) {
                if (pingTimes > MAX_PING_TIMES) {
                    LOG.warn("===服务端===(WRITER_IDLE 写超时): {},{}" , clientAddress,pingTimes);
                    writeResponse(ctx, BAD_REQUEST, "SERVER WRITER_IDLE");
                } else {
                    LOG.debug("Got ping response for client:{},{},{}", clientAddress, event.state(),pingTimes);
                    pingTimes++;
                }
            }
        }
    }

    private void tryComplete(ChannelHandlerContext ctx) {
        IOUtils.closeQuietly(randomFile);
        boolean bool = tempFile.renameTo(realFile);
        if (!bool) {
            boolean delete = tempFile.delete();
            LOG.warn("rename failed! delete temp file {} {}", tempFile.getPath(), delete);
            writeResponse(ctx, INTERNAL_SERVER_ERROR, "rename failed");
        } else {
            LOG.info("receive file success from client：{},save in {}", clientAddress, realFile.getPath());
            writeResponse(ctx, OK, "");
        }
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
        pingTimes = 0;
        if (msg instanceof HttpRequest) {
            HttpRequest request = (HttpRequest) msg;

            if (request.getUri().startsWith("download")
                    || request.getUri().startsWith("upload")
                    || request.getUri().startsWith("query")) {
                this.requestUri = request.getUri();
            } else {
                this.requestUri = TransferAuthService.decryptUrl(request.getUri());
            }

            if (requestUri.startsWith("download")) {
                downLoadResponse(ctx, request);
            } else if (requestUri.startsWith("upload")) {
                File tempFile = getUploadTempFile();
                totalFileLength = request.headers().getContentLength(request);
                randomFile = new RandomAccessFile(tempFile, "rw");
                if (isBreakPointUpload) {
                    receiveTotal = tempFile.length();
                    randomFile.seek(receiveTotal);
                } else {
                    receiveTotal = 0;
                }
                LOG.info("receive file start: {} ,{}/{}", tempFile, receiveTotal, totalFileLength);
            } else if (requestUri.startsWith("query")) {
                File tempFile = getUploadTempFile();
                if(tempFile.exists()){
                    receiveTotal = tempFile.length();
                }
                HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, OK);
                response.headers().set("CONNECTION", HttpHeaders.Values.KEEP_ALIVE);
                HttpHeaders.setContentLength(response, receiveTotal);
                ChannelFuture future = ctx.writeAndFlush(response);
                future.addListener(ChannelFutureListener.CLOSE);
                ctx.close();
            } else {
                LOG.warn("拦截到一个非法请求:{} ,url:{}", clientAddress, request.getUri());
                writeResponse(ctx, FORBIDDEN, "NO AUTH!");
            }

        }

        if (msg instanceof HttpContent) {
            // 接收客户端上传的文件
            if (requestUri.startsWith("upload")) {
                HttpContent chunk = (HttpContent) msg;
                ByteBuf buf = chunk.content();
                receiveTotal += buf.readableBytes();
                if (totalFileLength != 0) {
                    double rate = receiveTotal * 100 / totalFileLength;
                    LOG.debug("upload file rate：{} ==> {} %", tempFile.getAbsolutePath(), rate);
                }
                byte[] req = new byte[buf.readableBytes()];
                buf.readBytes(req);
                randomFile.write(req);
                if (chunk instanceof LastHttpContent) {
                    tryCompleteUpload(ctx);
                }
            }
        }
    }

    private void tryCompleteUpload(ChannelHandlerContext ctx) {
        if (receiveTotal == totalFileLength) {
            tryComplete(ctx);
        } else {
            //文件不完整
            LOG.info("file not complete {}，{} / {}", tempFile.getPath(), receiveTotal, totalFileLength);
            writeResponse(ctx, BAD_REQUEST, "file not complete");
        }
    }

    private File getUploadTempFile() throws IOException {
        QueryStringDecoder decoderQuery = new QueryStringDecoder(requestUri);
        Map<String, List<String>> uriAttributes = decoderQuery.parameters();
        isBreakPointUpload = Boolean.parseBoolean(uriAttributes.get("isBreak").get(0));
        String dataId = uriAttributes.get("dataId").get(0);
        String signature = uriAttributes.get("signature").get(0);
        if(signature==null ||"".equals(signature.trim())){
            signature = uriAttributes.get("version").get(0);
        }
        String uploadPath = PathBuilder.builderFileUploadPath(dataId, signature);
        realFile = new File(uploadPath);
        String tempUploadPath = PathBuilder.builderTempUploadPath(dataId, signature);
        tempFile = new File(tempUploadPath);
        if (!tempFile.getParentFile().exists()) {
            tempFile.getParentFile().mkdirs();
        }
        return tempFile;
    }

    private void downLoadResponse(final ChannelHandlerContext ctx, HttpRequest request) throws Exception {
        final File downFile;

        try {
            downFile = getDownFile();
        } catch (Exception e) {
            LOG.error("download file failed!",e);
            writeResponse(ctx, INTERNAL_SERVER_ERROR, "swift server error");
            return;
        }

        if (downFile == null || !downFile.exists()) {
            LOG.error("download file failed! file not found:{}, ->{}" , downFile ,clientAddress);
            writeResponse(ctx, NOT_FOUND, "file not found");
            return;
        }

        randomFile = new RandomAccessFile(downFile, "r");
        long count = randomFile.length() - downLength;
        HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
        HttpHeaders.setContentLength(response, count);
        setContentTypeHeader(response, downFile);
        if (HttpHeaders.isKeepAlive(request)) {
            response.headers().set("CONNECTION", HttpHeaders.Values.KEEP_ALIVE);
        }
        ctx.write(response);
        ChannelFuture sendFileFuture;
        sendFileFuture = ctx.channel().write(new DefaultFileRegion(randomFile.getChannel(), downLength, count),
                ctx.newProgressivePromise());
        sendFileFuture.addListener(new ChannelProgressiveFutureListener() {

            public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) {
                pingTimes = 0;
                LOG.debug("Transfer progress: {}/{}", progress, total);
            }

            public void operationComplete(ChannelProgressiveFuture future) throws IOException {
                if (future.isSuccess()) {
                    LOG.info("transfer to client success:{},{}", clientAddress, downFile);
                    writeResponse(ctx, OK, "");
                } else {
                    LOG.info("transfer to client failed:{},{},{}", clientAddress, downFile, future.cause());
                    writeResponse(ctx, SERVICE_UNAVAILABLE, "transfer failed");
                }
            }
        });

        ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
    }

    public File getDownFile() {
        Map<String, List<String>> uriAttributes = new QueryStringDecoder(requestUri).parameters();
        downLength = Long.parseLong(uriAttributes.get("downLength").get(0));
        String dataId = uriAttributes.get("dataId").get(0);
        String fileName = uriAttributes.get("signature").get(0);
        if(fileName==null ||"".equals(fileName.trim())){
            fileName = uriAttributes.get("version").get(0);
        }
        String filePath = PathBuilder.builderFileDownloadPath(dataId, fileName);
        File downloadFile = new File(filePath);
        //从本地缓存获取
        if (downloadFile.exists()) {
            LOG.info("download file from local cache:{}, -> {}", filePath,clientAddress);
            return downloadFile;
        }

        //从本地缓存获取不到,从本地待上传文件夹获取
        filePath = PRE_UPLOAD_FILE_DIR + dataId + File.separator + fileName;
        downloadFile = new File(filePath);
        if (downloadFile.exists()) {
            LOG.info("download file from preUpload dir:{}, -> {}", filePath,clientAddress);
            return downloadFile;
        }

        //从本地缓存获取不到,从本地上传文件夹获取
        filePath = PathBuilder.builderFileUploadPath(dataId, fileName);
        downloadFile = new File(filePath);
        if (downloadFile.exists()) {
            LOG.info("download file from upload dir:{}, -> {}", filePath ,clientAddress);
            return downloadFile;
        }

        //从云端下载
        LOG.info("download file from storage cloud:{}, -> {}", filePath ,clientAddress);
        StorageServiceFactory.getStorageService().downloadFile(dataId, fileName,filePath);
        return new File(filePath);
    }

    private void writeResponse(ChannelHandlerContext ctx, HttpResponseStatus status, String error) {
        HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status);
        response.headers().set("error", error);
        ChannelFuture future = ctx.writeAndFlush(response);
        future.addListener(ChannelFutureListener.CLOSE);
        ctx.close();
    }

    private void setContentTypeHeader(HttpResponse response, File file) {
        MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
        response.headers().set(CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath()));
    }

}
